package com.such.kit.io.template;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONReader;
import com.such.kit.Callable;
import com.such.kit.io.simple.support.IOSimple;
import com.such.kit.io.source.DatabaseIOSource;
import com.such.kit.io.support.IOSource;
import com.such.kit.io.template.support.IOTemplate;
import com.such.kit.placeholder.PlaceholderCommons;
import com.such.kit.stream.StreamCommons;
import com.such.kit.string.StringCommons;
import com.such.kit.validate.ValidateCommons;

/**
 * @author SUCH
 * <pre>
 * [继承扩展于 {@link IOTemplate}] 的 json 类型 IO 工具类
 * 
 * TextIOTemplate 的模板介质为 json 文件
 * TextIOTemplate 的模板格式基于 json 扩展，扩展项为 {@link #IO_CHARSET}、{@link #IO_INPUT_EXPRESSION}、{@link #IO_OUTPUT_EXPRESSION}
 * TextIOTemplate 的模板为对象形式的 json，如 {}
 * TextIOTemplate 的数据可以通过设置 {@link #rootType} 指定形式
 * 模板定义的是数据数组中 1 个元素
 * 待取值节点需要使用 {@link PlaceholderCommons 占位符}
 *   [导入时] 数据文件节点会与模板文件节点相对应，#[tableName] 会提取出 tableName 作 key，生成数据 {tableName: value}
 *   [导出时] 数据可以被待取值节点中的 {@link PlaceholderCommons 占位符} 由于，最终输出值
 * 
 * [举例] 以 {@link DatabaseIOSource} 为类
 * {
 *   IO_CHARSET: "UTF-8", 
 *   IO_INPUT_EXPRESSION: "insert into TABLE_INFO (schema, name, comment) value (:schema, :tableName, :tableComment)", 
 *   IO_OUTPUT_EXPRESSION: "select schema as schema, name as tableName, comment as tableComment from TABLE_INFO", 
 *   schema: "#[schema]", 
 *   tableName: "#[tableName]", 
 *   tableType: "#[tableType]", 
 *   tableComment: "#[tableComment]", 
 *   columns: [
 *     {
 *       IO_INPUT_EXPRESSION: "insert into COLUMN_INFO (schema, tableName, columnName) value (:schema, :tableName, :columnName)", 
 *       IO_OUTPUT_EXPRESSION: "
 *           select schema, tableName, columnName, dataType, columnSize, decimalDigits, defaultValue, notNull, autoincrement, columnComment 
 *           from COLUMN_INFO 
 *           where schema = :schema and tableName = :tableName"
 *       ", 
 *       schema: "#[schema]", 
 *       tableName: "#[tableName]", 
 *       columnName: "#[columnName]", 
 *       dataType: "#[dataType]", 
 *       columnSize: "#[columnSize]", 
 *       decimalDigits: "#[decimalDigits]", 
 *       defaultValue: "#[defaultValue]", 
 *       notNull: "#[notNull]", 
 *       autoincrement: "#[autoincrement]", 
 *       columnComment: "#[columnComment]"
 *     }
 *   ], 
 *   primaryKeys: [
 *     {
 *       IO_OUTPUT_EXPRESSION: "select distinct schema, tableName, pkName from PRIMARY_KEY_INFO where schema = :schema and table_name = :tableName", 
 *       schema: "#[schema]", 
 *       tableName: "#[tableName]", 
 *       pkName: "#[pkName]", 
 *       pkColumnNames: [
 *         #[pkColumnName], 
 *         {
 *           IO_INPUT_EXPRESSION: "insert into PRIMARY_KEY_INFO (schema, tableName, pkName, pkColumnName) value (:schema, :tableName, :pkName, :pkColumnName)", 
 *           IO_OUTPUT_EXPRESSION: "select pkColumnName from PRIMARY_KEY_INFO where schema = :schema and tableName = :tableName and pkName = :pkName"
 *         }
 *       ]
 *     }
 *   ]
 * }
 * 表达式可以引用数据、{@link IOSimple#args 自定义参数集} 中的值，其中引用数据遵循以下规则
 * [导入时] {@link #IO_INPUT_EXPRESSION} 可在 json 文档内引用所有上级节点、同级叶子节点、直属下级叶子节点中的数据
 * [导出时] {@link #IO_OUTPUT_EXPRESSION} 可在 json 文档内引用所有上级节点、同级叶子节点中的数据，同时输出数据到本节点以及直属下级叶子节点
 * 
 * 当模板节点为纯字符串或数组套数组时，无法在此节点上增加扩展项目，因此约定第 2 个元素为扩展对象，如 pkColumnNames 节点中的第 2 个元素
 * </pre>
 */
public class JSONIOTemplate extends IOTemplate {

	/** ROOT_TYPE_OBJECT：数据文件最外层为 {} */
	public static final String ROOT_TYPE_OBJECT = "object";
	/** ROOT_TYPE_ARRAY：数据文件最外层为 [] */
	public static final String ROOT_TYPE_ARRAY = "array";

	/** IO_CHARSET：数据文件编码，只能在根节点中定义 [默认值为 {@link StringCommons#CHARSET_GBK}] [参考 {@link StringCommons}.ENCODE_*] */
	public static final String IO_CHARSET = "IO_CHARSET";
	/**
	 * IO_INPUT_EXPRESSION：[导入时] 表达式
	 * <pre>
	 * {@link IOSource} 的不同实现 IO_INPUT_EXPRESSION 的推荐值有所变化
	 * 以 {@link DatabaseIOSource} 为例，IO_INPUT_EXPRESSION 的推荐值为 sql 语句
	 * [举例] ["insert into TABLE_INFO (schema, name, comment) value (:schema, :tableName, :tableComment)"]
	 * </pre>
	 */
	public static final String IO_INPUT_EXPRESSION = "IO_INPUT_EXPRESSION";
	/**
	 * IO_OUTPUT_EXPRESSION：[导出时] 表达式
	 * <pre>
	 * {@link IOSource} 的不同实现 IO_OUTPUT_EXPRESSION 的推荐值有所变化
	 * 以 {@link DatabaseIOSource} 为例，IO_OUTPUT_EXPRESSION 的推荐值为 sql 语句
	 * [举例] "select schema as schema, name as tableName, comment as tableComment from TABLE_INFO"
	 * </pre>
	 */
	public static final String IO_OUTPUT_EXPRESSION = "IO_OUTPUT_EXPRESSION";

	private JSONObject templateJSONObject;
	private Map<String, Object> fieldValueMap;
	/**
	 * rootType：指定最外层（root）的类型 [{@link #ROOT_TYPE_OBJECT} | {@link #ROOT_TYPE_ARRAY}] [默认值为 {@link #ROOT_TYPE_ARRAY}]
	 * <pre>
	 * [导入时] {@link #ROOT_TYPE_ARRAY} 模式下将开启“边读取边解析”，以处理大 json 文本
	 * [导出时] {@link #ROOT_TYPE_ARRAY} 模式下会自动在最终输出文本前后添加 []
	 * </pre>
	 */
	private String rootType = ROOT_TYPE_ARRAY;
	/** filterEmptyNode：[导出时] 过滤空的节点 [true 过滤 | false 不过滤] [默认为 false] */
	private boolean filterEmptyNode = false;

	/** [参考 {@link IOTemplate#IOTemplate(String)}] */
	public JSONIOTemplate(String templatePath) throws Exception {
		super(templatePath);
	}

	/** [参考 {@link IOTemplate#IOTemplate(String, String, IOSource)}] */
	public JSONIOTemplate(String templateString, String charset) throws Exception {
		super(templateString, charset);
	}

	/** [参考 {@link IOTemplate#IOTemplate(byte[], IOSource)}] */
	public JSONIOTemplate(byte[] templateBytes) throws Exception {
		super(templateBytes);
	}

	/** [参考 {@link IOTemplate#IOTemplate(InputStream, IOSource)}] */
	public JSONIOTemplate(InputStream templateIn) throws Exception {
		super(templateIn);
	}

	protected void initTemplate() throws Exception {
		if (ValidateCommons.isEmpty(this.templateJSONObject)) {
			JSONReader templateReader = new JSONReader(new InputStreamReader(super.templateIn));
			try {
				this.templateJSONObject = (JSONObject) templateReader.readObject();
				String charset = this.templateJSONObject.getString(IO_CHARSET);
				if (ValidateCommons.isNotEmpty(charset)) {
					super.charset = charset;
					// 删除编码节点
					this.templateJSONObject.remove(IO_CHARSET);
				}
			} finally {
				StreamCommons.closePure(templateReader);
			}
		}
	}

	@Override
	public void setCharset(String charset) {
		super.charset = charset;
	}

	@Override
	public void putArg(String key, Object value) {
		super.args.put(key, value);
	}

	@Override
	public void putArgs(Map<String, Object> args) {
		super.args.putAll(args);
	}

	@Override
	public void clearArgs() {
		super.args.clear();
	}

	@Override
	public String getFileType() {
		return "json";
	}

	/**
	 * <pre>
	 * 指定 {@link #rootType}
	 * </pre>
	 * @param rootType [参考 {@link #rootType}]
	 */
	public void setRootType(String rootType) {
		this.rootType = rootType;
	}

	/**
	 * <pre>
	 * 指定 {@link #filterEmptyNode}
	 * </pre>
	 * @param filterEmptyNode [参考 {@link #filterEmptyNode}]
	 */
	public void setFilterEmptyNode(boolean filterEmptyNode) {
		this.filterEmptyNode = filterEmptyNode;
	}

	@Override
	public void input(IOSource ioSource, InputStream resourceIn) throws Exception {
		JSONReader reader = new JSONReader(new InputStreamReader(resourceIn, super.charset));
		try {
			if (ROOT_TYPE_ARRAY.equals(this.rootType)) {
				reader.startArray();
				while(reader.hasNext()) {
					this.fieldValueMap = new HashMap<String, Object>(super.args);
					inputJSONObject(ioSource, this.templateJSONObject, (JSONObject) reader.readObject());
				}
				reader.endArray();
			} else if (ROOT_TYPE_OBJECT.equals(this.rootType)) {
				this.fieldValueMap = new HashMap<String, Object>(super.args);
				inputJSONObject(ioSource, this.templateJSONObject, (JSONObject) reader.readObject());
			}
		} finally {
			StreamCommons.closePure(reader);
		}
		ioSource.doInput(null, null, true, null);
	}

	private void inputJSONObject(IOSource ioSource, JSONObject template, JSONObject resource) {
		if (ValidateCommons.isNotEmpty(resource)) {
			Set<String> fieldSet = template.keySet();

			// 取所有 String 类型的值构建 fieldValueMap，并执行 doInput
			for (String field : fieldSet) {
				if (IO_INPUT_EXPRESSION.equals(field) || IO_OUTPUT_EXPRESSION.equals(field)) {
					continue;
				}
				Object typeChecker = template.get(field);
				if (typeChecker instanceof String) {
					String innerKey = template.getString(field);
					String key = PlaceholderCommons.getInnerKey(innerKey);
					if (ValidateCommons.isNotEmpty(key)) {
						String value = resource.getString(field);
						if (ValidateCommons.isEmpty(value)) {
							value = "";
						}
						this.fieldValueMap.put(key, value);
					}
				}
			}
			String ie = template.getString(IO_INPUT_EXPRESSION);
			if (ValidateCommons.isNotEmpty(ie)) {
				Map<String, Object> fieldValueMap = new HashMap<String, Object>(this.fieldValueMap);
				ioSource.doInput(ie, fieldValueMap, false, new HashMap<String, Object>());
			}

			// 对 JSONObject、JSONArray 类型的值进行递归
			for (String field : fieldSet) {
				Object typeChecker = template.get(field);
				if (typeChecker instanceof JSONObject) {
					inputJSONObject(ioSource, template.getJSONObject(field), resource.getJSONObject(field));
				} else if (typeChecker instanceof JSONArray) {
					inputJSONArray(ioSource, template.getJSONArray(field), resource.getJSONArray(field));
				}
			}
		}
	}

	private void inputJSONArray(IOSource ioSource, JSONArray template, JSONArray resource) {
		if (ValidateCommons.isNotEmpty(resource)) {
			// 取数组第 1 个元素（既模板元素）用于数组元素类型判断
			Object typeChecker = template.get(0);
			if (typeChecker instanceof String) {
				/* 元素类型为 String
				 * 第 1 个元素为模板，当模板中无占位符则保持模板原样
				 * 第 2 个元素为 config
				 */
				String innerKey = template.getString(0);
				String key = PlaceholderCommons.getInnerKey(innerKey);
				if (ValidateCommons.isNotEmpty(key)) {
					// 当模板中有占位符
					String ie = "";
					if (template.size() > 1) {
						// 取第 2 个元素并检查是否为 config（JSONObject 类型）
						Object configChecker = template.get(1);
						if (configChecker instanceof JSONObject) {
							JSONObject config = template.getJSONObject(1);
							ie = config.getString(IO_INPUT_EXPRESSION);
						}
					}
					for (int i = 0; i < resource.size(); i++) {
						String value = resource.getString(i);
						if (ValidateCommons.isEmpty(value)) {
							value = "";
						}
						this.fieldValueMap.put(key, value);
						if (ValidateCommons.isNotEmpty(ie)) {
							Map<String, Object> fieldValueMap = new HashMap<String, Object>(this.fieldValueMap);
							ioSource.doInput(ie, fieldValueMap, false, new HashMap<String, Object>());
						}
					}
				}
			} else if (typeChecker instanceof JSONObject) {
				// 元素类型为 JSONObject
				for (int i = 0; i < resource.size(); i++) {
					inputJSONObject(ioSource, template.getJSONObject(0), resource.getJSONObject(i));
				}
			} else if (typeChecker instanceof JSONArray) {
				// 元素类型为 JSONArray
				for (int i = 0; i < resource.size(); i++) {
					inputJSONArray(ioSource, template.getJSONArray(0), resource.getJSONArray(i));
				}
			}
		}
	}

	@Override
	public void output(IOSource ioSource, OutputStream resourceOut) throws Exception {
		if (ROOT_TYPE_ARRAY.equals(this.rootType)) {
			resourceOut.write('[');
			outputJSONObject(ioSource, this.templateJSONObject, null, true, resourceOut);
			resourceOut.write(']');
		} else if (ROOT_TYPE_OBJECT.equals(this.rootType)) {
			outputJSONObject(ioSource, this.templateJSONObject, null, true, resourceOut);
		}
	}

	private JSONObject outputJSONObject(IOSource ioSource, JSONObject template, Map<String, Object> params, boolean isRoot, OutputStream out) throws Exception {
		if (ValidateCommons.isEmpty(params)) {
			params = new HashMap<String, Object>();
		}
		params.putAll(super.args);

		JSONObject resource = new JSONObject();
		String oe = template.getString(IO_OUTPUT_EXPRESSION);
		if (ValidateCommons.isNotEmpty(oe)) {
			ioSource.doOutput(oe, params, new OutputCallable(ioSource, resource, template).setRoot(isRoot).setOut(out));
		} else {
			appendJSONObject(ioSource, template, resource, params, isRoot, out);
		}
		return resource;
	}

	private JSONArray outputJSONArray(IOSource ioSource, JSONArray template, Map<String, Object> params) throws Exception {
		if (ValidateCommons.isEmpty(params)) {
			params = new HashMap<String, Object>();
		}
		params.putAll(super.args);

		JSONArray resource = new JSONArray();
		Object typeChecker = template.get(0);
		if (typeChecker instanceof String) {
			String innerKey = template.getString(0);
			String oe = "";
			if (template.size() > 1) {
				Object configChecker = template.get(1);
				if (configChecker instanceof JSONObject) {
					JSONObject config = template.getJSONObject(1);
					oe = config.getString(IO_OUTPUT_EXPRESSION);
				}
			}
			if (ValidateCommons.isNotEmpty(oe)) {
				ioSource.doOutput(oe, params, new OutputCallable(ioSource, resource, innerKey));
			} else {
				resource.add(PlaceholderCommons.replacePlaceholder(innerKey, params));
			}
		} else if (typeChecker instanceof JSONObject) {
			JSONObject templateJO = template.getJSONObject(0);
			String oe = templateJO.getString(IO_OUTPUT_EXPRESSION);
			if (ValidateCommons.isNotEmpty(oe)) {
				ioSource.doOutput(oe, params, new OutputCallable(ioSource, resource, templateJO));
			} else {
				resource.add(outputJSONObject(ioSource, templateJO, params, false, null));
			}
		} else if (typeChecker instanceof JSONArray) {
			JSONArray templateJA = template.getJSONArray(0);
			String oe = "";
			if (templateJA.size() > 1) {
				Object configChecker = templateJA.get(1);
				if (configChecker instanceof JSONObject) {
					JSONObject config = templateJA.getJSONObject(1);
					oe = config.getString(IO_OUTPUT_EXPRESSION);
				}
			}
			if (ValidateCommons.isNotEmpty(oe)) {
				ioSource.doOutput(oe, params, new OutputCallable(ioSource, resource, templateJA));
			} else {
				resource.add(outputJSONArray(ioSource, templateJA, params));
			}
		}
		return resource;
	}

	private boolean appendComma = false;
	private void appendJSONObject(IOSource ioSource, JSONObject template, JSONObject resource, Map<String, Object> params, boolean isRoot, OutputStream out) throws Exception {
		Set<String> fieldSet = template.keySet();
		// 先单独处理完所有字符串类型
		for (String field : fieldSet) {
			if (IO_INPUT_EXPRESSION.equals(field) || IO_OUTPUT_EXPRESSION.equals(field)) {
				continue;
			}
			String key = PlaceholderCommons.replacePlaceholder(field, params);
			Object typeChecker = template.get(field);
			if (typeChecker instanceof String) {
				String value = template.getString(field);
				value = PlaceholderCommons.replacePlaceholder(template.getString(field), params);
				if (this.filterEmptyNode && ValidateCommons.isNotEmpty(value)) {
					resource.put(key, value);
				} else {
					resource.put(key, value);
				}
			}
		}
		for (String field : fieldSet) {
			String key = PlaceholderCommons.replacePlaceholder(field, params);
			Object typeChecker = template.get(field);
			if (typeChecker instanceof JSONObject) {
				JSONObject jsonObject = outputJSONObject(ioSource, template.getJSONObject(field), params, false, null);
				if (this.filterEmptyNode && !jsonObject.isEmpty()) {
					resource.put(key, jsonObject);
				} else {
					resource.put(key, jsonObject);
				}
			} else if (typeChecker instanceof JSONArray) {
				JSONArray jsonArray = outputJSONArray(ioSource, template.getJSONArray(field), params);
				if (this.filterEmptyNode && !jsonArray.isEmpty()) {
					resource.put(key, jsonArray);
				} else {
					resource.put(key, jsonArray);
				}
			}
		}

		if (isRoot && ValidateCommons.isNotEmpty(out, false)) {
			if (this.appendComma) {
				out.write(',');
				out.write(' ');
			} else {
				this.appendComma = true;
			}
			out.write(resource.toJSONString().getBytes(charset));
		}
	}

	private class OutputCallable implements Callable<Map<String, Object>, Boolean> {

		private IOSource ioSource;
		private JSONObject resourceJO;
		private JSONArray resourceJA;
		private JSONObject templateJO;
		private JSONArray templateJA;
		private String fieldValue;
		private String fieldKey;
		private boolean isRoot = false;
		private OutputStream out = null;

		public OutputCallable(IOSource ioSource, JSONObject resource, JSONObject template) {
			this.ioSource = ioSource;
			this.resourceJO = resource;
			this.templateJO = template;
		}

		public OutputCallable(IOSource ioSource, JSONObject resource, JSONArray template, String fieldKey) {
			this.ioSource = ioSource;
			this.resourceJO = resource;
			this.templateJA = template;
			this.fieldKey = fieldKey;
		}

		public OutputCallable(IOSource ioSource, JSONObject resource, String fieldValue, String fieldKey) {
			this.ioSource = ioSource;
			this.resourceJO = resource;
			this.fieldValue = fieldValue;
			this.fieldKey = fieldKey;
		}

		public OutputCallable(IOSource ioSource, JSONArray resource, JSONObject template) {
			this.ioSource = ioSource;
			this.resourceJA = resource;
			this.templateJO = template;
		}

		public OutputCallable(IOSource ioSource, JSONArray resource, JSONArray template) {
			this.ioSource = ioSource;
			this.resourceJA = resource;
			this.templateJA = template;
		}

		public OutputCallable(IOSource ioSource, JSONArray resource, String fieldValue) {
			this.ioSource = ioSource;
			this.resourceJA = resource;
			this.fieldValue = fieldValue;
		}

		public OutputCallable setRoot(boolean isRoot) {
			this.isRoot = isRoot;
			return this;
		}

		public OutputCallable setOut(OutputStream out) {
			this.out = out;
			return this;
		}

		@Override
		public Boolean call(Map<String, Object> params) throws Exception {
			if (ValidateCommons.isNotEmpty(this.resourceJO)) {
				if (ValidateCommons.isNotEmpty(this.templateJO)) {
					appendJSONObject(this.ioSource, this.templateJO, this.resourceJO, params, this.isRoot, this.out);
				} else if (ValidateCommons.isNotEmpty(this.templateJA) && ValidateCommons.isNotEmpty(this.fieldKey)) {
					this.resourceJO.put(this.fieldKey, outputJSONArray(this.ioSource, this.templateJA, params));
				} else if (ValidateCommons.isNotEmpty(this.fieldValue) && ValidateCommons.isNotEmpty(this.fieldKey)) {
					this.resourceJO.put(this.fieldKey, PlaceholderCommons.replacePlaceholder(this.fieldValue, params));
				}
			} else if (ValidateCommons.isNotEmpty(this.resourceJA)) {
				if (ValidateCommons.isNotEmpty(this.templateJO)) {
					JSONObject resource = new JSONObject();
					appendJSONObject(this.ioSource, this.templateJO, resource, params, this.isRoot, this.out);
					this.resourceJA.add(resource);
				} else if (ValidateCommons.isNotEmpty(this.templateJA)) {
					this.resourceJA.add(outputJSONArray(this.ioSource, this.templateJA, params));
				} else if (ValidateCommons.isNotEmpty(this.fieldValue)) {
					this.resourceJA.add(PlaceholderCommons.replacePlaceholder(this.fieldValue, params));
				}
			}
			return true;
		}

	}

}
